package lzw;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import main.Compress;
import main.Decompress;

public class LZW implements Compress, Decompress{
    public static final int INITIAL_DICT_SIZE = 256;
    public static final int DEFAULT_CODEWORD_LENGTH = 12;

    private int codeWordLength = DEFAULT_CODEWORD_LENGTH;
    private Map<ByteArray, Integer> codeTable;
    private List<ByteArray> decodeTable;
    
	public void compress(File inFile, File outFile) {
		try {
			InputStream inputStream = new FileInputStream(inFile);
			OutputStream outputStream = new FileOutputStream(outFile);
			compress(inputStream, outputStream);
		}catch (Exception e){
			e.printStackTrace();
		}
	}
	/**
	 * Compress a FILE (not a DIR) to output stream. Ignore all descriptors of the 
	 * file -- name, size, address, etc.
	 * @author AI Wei
	 * @param inFile
	 * @param outputStream
	 * @throws IOException 
	 */
	private void compress(InputStream inStream, OutputStream outStream) throws IOException{
		// Initialization
		codeTable = new HashMap<ByteArray, Integer>();
        for (int i = 0; i < INITIAL_DICT_SIZE; i++) {
                codeTable.put(new ByteArray((byte) i), i);
        }
        int code = INITIAL_DICT_SIZE;
        int maxCode = (1 << codeWordLength) - 1;
        
        InputStream bufferedIn = new BufferedInputStream(inStream);
        MyOutput compressedOutput = new MyOutput(new BufferedOutputStream(outStream), codeWordLength);
        
        int firstByte = bufferedIn.read();
        ByteArray w = new ByteArray((byte) firstByte);
        int K;

        while ((K = bufferedIn.read()) != -1) {
                ByteArray wK = new ByteArray(w).append((byte) K);
                if (codeTable.containsKey(wK)) {
                        w = wK;
                } else {
                        compressedOutput.write(codeTable.get(w));
                        if (code < maxCode) {
                                codeTable.put(wK, code++);
                        }
                        w = new ByteArray((byte) K);
                }
        }
        compressedOutput.write(codeTable.get(w));
        compressedOutput.flush();               
	}
	public boolean decompress(File inFile, File outFile) {
		try {
			InputStream inputStream = new FileInputStream(inFile);
			OutputStream outputStream = new FileOutputStream(outFile);
			return decompress(inputStream, outputStream);
		}catch (Exception e){
			e.printStackTrace();
		}
		return false;
	}
	private boolean decompress(InputStream inStream, OutputStream outStream) throws IOException {
        decodeTable = new ArrayList<ByteArray>();
        for (int i = 0; i < INITIAL_DICT_SIZE; i++) {
            decodeTable.add(new ByteArray((byte) i));
        }
        MyInput compressedInput = new MyInput(new BufferedInputStream(inStream), codeWordLength);
        OutputStream bufferedOut = new BufferedOutputStream(outStream);

        int oldCode = compressedInput.read();
        bufferedOut.write(oldCode);
        int character = oldCode;
        int newCode;
        while ((newCode = compressedInput.read()) != -1) {
                ByteArray string;
                if (newCode >= decodeTable.size()) {
                        string = new ByteArray(decodeTable.get(oldCode));
                        string.append((byte) character);
                } else {
                        string = decodeTable.get(newCode);
                }
                for (int i = 0; i < string.size(); i++) {
                        bufferedOut.write(string.get(i));
                }
                character = string.get(0);
                decodeTable.add(new ByteArray(decodeTable.get(oldCode))
                                .append((byte) character));
                oldCode = newCode;
        }

        bufferedOut.flush();
		return true;
	}	
}
